/**
 * Copyright (c) 2007-2008 Nearchos Paspallis
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * AspectSense
 *
 * Middleware for annotation-based, context-aware and adaptive services for
 * mobile and ubiquitous computing
 *
 * This project is an open-source effort, initiated by Nearchos Paspallis as
 * part of his PhD work at the University of Cyprus (http://www.ucy.ac.cy).
 *
 * More information about the project is available at "http://aspectsense.org".
 */

package org.aspectsense.framework;

import org.aspectsense.context.events.ContextChangedEvent;
import org.aspectsense.context.*;
import org.aspectsense.metadata.MetadataFactory;
import org.aspectsense.evaluators.UtilityEvaluator;
import org.aspectsense.evaluators.EvaluationException;
import org.aspectsense.coreservices.AdaptationService;
import org.aspectsense.coreservices.ConfigurationService;
import org.aspectsense.coreservices.ContextService;

import java.util.logging.Logger;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * @author Nearchos Paspallis [nearchos@cs.ucy.ac.cy]
 *         Date: Dec 5, 2007
 *         Time: 3:57:45 PM
 */
public class AdaptationManager implements AdaptationService
{
    public static final int REQUIRED_ACTION_NONE                            = 0;
    public static final int REQUIRED_ACTION_SELECT_SERVICES                 = 1;
    public static final int REQUIRED_ACTION_EVALUATE_SERVICE_UTILITY        = 2;
    public static final int REQUIRED_ACTION_EVALUATE_SERVICE_PROVISION      = 3;
    public static final int REQUIRED_ACTION_EVALUATE_COMPONENT_AVAILABILITY = 4;

    public static final long SLEEP_INTERVAL_WHEN_NO_MESSAGES = 500l; // 1/2 sec

    public static final int MAX_EVENT_QUEUE_CAPACITY = 256;

    public static final double DEFAULT_USER_NEED = 0.5d;

    private final Logger logger = Logger.getLogger(AdaptationManager.class.getCanonicalName());

    private final Repository repository = Repository.getInstance();

    private ConfigurationService configurationService = null;

    // ----------------- Singleton pattern implementation ------------------- //

    private static AdaptationManager instance = null;

    /**
     * Provides access to the lazy-loaded singleton instance of the
     * {@link AdaptationManager}.
     *
     * @return an instance of {@link AdaptationManager}
     */
    static AdaptationManager getInstance()
    {
        synchronized(AdaptationManager.class)
        {
            if(instance == null) instance = new AdaptationManager();

            return instance;
        }
    }

    private AdaptationManager()
    {
        logger.fine("Starting instance of " + AdaptationManager.class.getCanonicalName());

        new Thread(new EventProcessor()).start();
    }

    // ----------------------- Instance-wide variables ---------------------- //

    /**
     * Maps component IDs to availability, i.e. whether the component can be
     * deployed or not based on the required resources. For example, a component
     * that requires networking will be unavailable when networking is not
     * provided.
     */
    private final Map<String, Boolean> componentAvailability
            = new HashMap<String, Boolean>();

    /**
     * Contains the IDs of the services which are available remotely.
     */
    private final Set<String> providedServices = new HashSet<String>();

    /**
     * Maps service IDs to utilities, i.e. a value from 0 to 1 indicating the
     * benefit offered from the service to the user.
     */
    private final Map<String, ServiceProvidersUtility> serviceProviderUtilities
            = new HashMap<String, ServiceProvidersUtility>();

    /**
     * Maps the available end-user services to a weight corresponding to the
     * importance it has for her or him.
     */
    private final Map<String, Double> userNeededServices
            = new HashMap<String, Double>();

    // -------------------- End of Instance-wide variables ------------------ //

    // --------------- End of singleton pattern implementation -------------- //

    public void setContextService(final ContextService contextService)
    {
        contextService.addContextListener(DefaultQuery.ANY_CONTEXT_CHANGE, this);
    }

    public void setConfigurationService(final ConfigurationService configurationService)
    {
        this.configurationService = configurationService;
    }

    public Map<String, Boolean> getEndUserServiceAvailability()
    {
        final Map<String, Boolean> endUserServiceAvailabilityMap
                = new HashMap<String, Boolean>();

        for(final String providedServiceID : providedServices)
        {
            if(MetadataFactory.isEndUserService(repository.getServiceType(providedServiceID)))
            {
                endUserServiceAvailabilityMap.put(providedServiceID, isServiceProvided(providedServiceID));
            }
        }

        return endUserServiceAvailabilityMap;
    }

    public Map<String, Double> getUserNeededServices()
    {
        return new HashMap<String, Double>(userNeededServices);
    }

    // -------- Handling the notifications from the context manager --------- //

    private final Queue<ContextChangedEvent> eventQueue
            = new LinkedBlockingQueue<ContextChangedEvent>(MAX_EVENT_QUEUE_CAPACITY);

    public void contextChanged(final ContextChangedEvent contextChangedEvent)
    {
        if(contextChangedEvent == null)
        {
            throw new NullPointerException("Illegal null argument");
        }

        logger.finest("Adaptation manager: Received context changed event: " + contextChangedEvent);

        synchronized(eventQueue)
        {
            eventQueue.add(contextChangedEvent);
        }
    }

    private class EventProcessor implements Runnable
    {
        public void run()
        {
            while(true)
            {
                if(eventQueue.isEmpty())
                {
                    try
                    {
                        Thread.sleep(SLEEP_INTERVAL_WHEN_NO_MESSAGES);
                    }
                    catch (InterruptedException ie)
                    {
                        logger.severe("Thread interrupted: " + ie.getMessage());
                        break;
                    }
                }

                int requiredAction = REQUIRED_ACTION_NONE;

                synchronized(eventQueue)
                {
                    // processing the events
                    while (!eventQueue.isEmpty())
                    {
                        final ContextChangedEvent event = eventQueue.poll();
                        final ContextType contextType = event.getQuery().getContextType();

                        if(contextType.isChildOf(DefaultContextType.USER_PREFERENCES_CONTEXT_TYPE))
                        {
                            // update user preferences
                            updateUserPreferences(event);

                            if(requiredAction < REQUIRED_ACTION_SELECT_SERVICES)
                            {
                                requiredAction = REQUIRED_ACTION_SELECT_SERVICES;
                            }
                        }
                        else if(contextType.equals(DefaultContextType.SERVICE_AVAILABILITY_CONTEXT_TYPE))
                        {
                            if(requiredAction < REQUIRED_ACTION_EVALUATE_SERVICE_PROVISION)
                            {
                                requiredAction = REQUIRED_ACTION_EVALUATE_SERVICE_PROVISION;
                            }
                        }
                        else if(contextType.equals(DefaultContextType.COMPONENT_AVAILABILITY_CONTEXT_TYPE))
                        {
                            if(requiredAction < REQUIRED_ACTION_EVALUATE_COMPONENT_AVAILABILITY)
                            {
                                requiredAction = REQUIRED_ACTION_EVALUATE_COMPONENT_AVAILABILITY;
                            }
                        }
                        else // general context type
                        {
                            if(requiredAction < REQUIRED_ACTION_EVALUATE_SERVICE_UTILITY)
                            {
                                requiredAction = REQUIRED_ACTION_EVALUATE_SERVICE_UTILITY;
                            }
                        }

System.out.println("processed event: " + event + " which triggered action: " + requiredAction);
                    }

                    logger.fine("Adaptation manager: required action: " + requiredAction);
if(requiredAction != REQUIRED_ACTION_NONE) System.out.println("Adaptation manager: required action: " + requiredAction);//todo
                }

                switch (requiredAction)
                {
                    case REQUIRED_ACTION_NONE:
                        break;
                    case REQUIRED_ACTION_SELECT_SERVICES:
                        selectServices();
                        break;
                    case REQUIRED_ACTION_EVALUATE_SERVICE_UTILITY:
                        evaluateServiceUtility();
                        break;
                    case REQUIRED_ACTION_EVALUATE_SERVICE_PROVISION:
                        evaluateServiceProvision();
                        break;
                    case REQUIRED_ACTION_EVALUATE_COMPONENT_AVAILABILITY:
                        evaluateComponentsAvailability();
                        break;
                    default:
                        throw new RuntimeException("Invalid action: " + requiredAction);
                }
            }
        }
    }

    // ----- End of handling the notifications from the context manager ----- //

    // --------------------- Main adaptation steps -------------------------- //

    /**
     * Step 1: Evaluate the availability of each installed component. For
     * instance, some components which require network should be defined as
     * unavailable when no networking is provided.
     */
    private void evaluateComponentsAvailability()
    {
        logger.fine("1. evaluateComponentsAvailability");


        synchronized (componentAvailability)
        {
            // todo: for now, assume that all installed components are available
            final String [] installedComponentIDs = repository.getInstalledComponentIDs();
            final Set<String> updatedComponentAvailability = new HashSet<String>();
            updatedComponentAvailability.addAll(Arrays.asList(installedComponentIDs));

            // first, handle removed components
            {
                final Set<String> currentComponentIDs = componentAvailability.keySet();
                for(final String currentComponentID : currentComponentIDs)
                {
                    if(!updatedComponentAvailability.contains(currentComponentID))
                    {
                        componentAvailability.remove(currentComponentID);
                    }
                }
            }

            // next, handle added components
            {
                for(final String installedComponentID : installedComponentIDs)
                {
                    if(!componentAvailability.containsKey(installedComponentID))
                    {
                        componentAvailability.put(installedComponentID, true);
                    }
                }
            }
        }

        // move on to next step
        evaluateServiceProvision();
    }

    /**
     * Step 2: Evaluate which services are provided by recursively resolving
     * the components that offer them. Also evaluateDouble what remote services are
     * available by inquiring a registry or directory.
     */
    private void evaluateServiceProvision()
    {
        logger.fine("2. evaluateServiceProvision");

        synchronized (providedServices)
        {
            final String [] serviceIDs = repository.getInstalledServiceIDs();
            final Set<String> updatedServiceIDs = new HashSet<String>();
            updatedServiceIDs.addAll(Arrays.asList(serviceIDs));

            // initially, handle removed services
            final Set<String> copyOfProvidedServices = new HashSet<String>(providedServices);
            for(final String providedServiceID : copyOfProvidedServices)
            {
                if(!updatedServiceIDs.contains(providedServiceID))
                {
                    providedServices.remove(providedServiceID);
                    if(MetadataFactory.isEndUserService(repository.getServiceType(providedServiceID)))
                    {
                        userNeededServices.remove(providedServiceID);
                    }
                }
            }

            // then, handle added services
            for(final String updatedServiceID : updatedServiceIDs)
            {
                if(!providedServices.contains(updatedServiceID))
                {
                    providedServices.add(updatedServiceID);
                    if(MetadataFactory.isEndUserService(repository.getServiceType(updatedServiceID)))
                    {
                        userNeededServices.put(updatedServiceID, DEFAULT_USER_NEED);
                    }
                }
            }
        }

        // todo: add services that are remotely available

        // move on to next step
        evaluateServiceUtility();
    }

    /**
     * Returns true if and only if there exists at least one component provider
     * for this service which is currently available.
     *
     * @param serviceID the ID of the service to be examined
     * @return true if and only if there exists at least one component provider
     * for this service which is currently available
     */
    private boolean isServiceProvided(final String serviceID)
    {
        // get all providers of the specified service ID
        final String [] serviceProviderComponentIDs = repository.getServiceProviders(serviceID);

        // evaluate whether there exists at least one service provider available
        for(final String serviceProviderComponentID : serviceProviderComponentIDs)
        {
            if(componentAvailability.get(serviceProviderComponentID))
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Step 3: Compute the utility of each service by each service provider.
     */
    private void evaluateServiceUtility()
    {
        logger.fine("3. evaluateServiceUtility");

        serviceProviderUtilities.clear();

        for(final String serviceID : providedServices)
        {
            final String [] serviceProviderIDs = repository.getServiceProviders(serviceID);
            final ServiceProvidersUtility serviceProvidersUtility = new ServiceProvidersUtility(serviceID);
            for(final String serviceProviderID : serviceProviderIDs)
            {
                final UtilityEvaluator utilityEvaluator
                        = repository.getServiceUtilityEvaluator(serviceID, serviceProviderID);

                try
                {
                    double utility = utilityEvaluator.evaluateDouble();
                    serviceProvidersUtility.putServiceProviderUtility(serviceProviderID, utility);
                }
                catch (EvaluationException ee)
                {
                    logger.severe("EvaluationException: " + ee.getMessage()
                            + ", caused by: " + ee.getCause());
                }
            }

            serviceProviderUtilities.put(serviceID, serviceProvidersUtility);
        }

        // move on to next step
        selectServices();
    }

    /**
     * Specifies the current resolution to allow to check if there are changes
     * before attempting to apply a reconfiguration.
     */
    private Map<String, String> currentServiceResolutionMatrix = null;

    private void updateUserPreferences(final ContextChangedEvent event)
    {
        final Query query = event.getQuery();
        final ContextType contextType = query.getContextType();

        if(contextType.isChildOf(DefaultContextType.USER_PREFERENCES_CONTEXT_TYPE))
        {
            final QueryResults queryResults = event.getQueryResults();
            final Set<ContextType> contextTypes = queryResults.getQueryResultTypes();
            for(final ContextType type : contextTypes)
            {
                final String serviceID = type.toString().substring(DefaultContextType.USER_PREFERENCES_CONTEXT_TYPE.toString().length() + 1);
                final ContextValue contextValue = queryResults.getValueForType(type);
                final double value = (Double) contextValue.getValue();

                userNeededServices.put(serviceID, value);
            }
        }
    }

    /**
     * Step 4:
     */
    private void selectServices()
    {
        logger.fine("4. selectServices");

        // update the user needed services and weights
        final Set<String> endUserServiceIDs = new HashSet<String>();

        final String [] serviceIDs = repository.getInstalledServiceIDs();
        for(final String serviceID : serviceIDs)
        {
            if(MetadataFactory.isEndUserService(repository.getServiceType(serviceID)))
            {
                endUserServiceIDs.add(serviceID);
            }
        }

        // select services based on the utility of the provided services and
        // on the user needs

        /** Maps service IDs to the corresponding providers (i.e. components)*/
        final Map<String, String> serviceResolutionMatrix = new HashMap<String, String>();

        for(final String endUserServiceID : endUserServiceIDs)
        {
            // evaluateDouble only the end user services actually needed by the user
            if(userNeededServices.get(endUserServiceID) > 0d)
            {
                // for each required service, recursively compute the resolution matrix
                final Stack<String> stack = new Stack<String>();
                // each unresolved service is put in the stack, and removed when resolved
                stack.push(endUserServiceID);
                while(!stack.isEmpty())
                {
                    final String serviceID = stack.pop();

                    final String componentProviderID
                            = serviceProviderUtilities.get(serviceID).getComponentWithMaxUtility();

                    logger.finest("Service provider utilities: " + serviceProviderUtilities.get(serviceID));

                    // a component provider ID cannot be null because before
                    // this step, the component availability, along with the
                    // resulting service availability are both checked!
                    assert componentProviderID != null;

                    serviceResolutionMatrix.put(serviceID, componentProviderID);

                    // update dependencies
                    final Class componentProviderType = repository.getComponentType(componentProviderID);
                    final Map<String, RequiredServiceMetadata> requiredServices =
                            MetadataFactory.getComponentMetadata(componentProviderType).getRequiredServices();
                    final Set<String> requiredServiceIDs = requiredServices.keySet();
                    for(final String requiredServiceID : requiredServiceIDs)
                    {
                        if(!serviceResolutionMatrix.containsKey(requiredServiceID))
                        {
                            stack.push(requiredServiceID);
                        }
                    }
                }
            }
        }

        // if there is no current configuration, and a new configuration is selected...
        if( (currentServiceResolutionMatrix == null && !serviceResolutionMatrix.isEmpty())
        // ... or if there is a current configuration and a different one is selected then...
                || (currentServiceResolutionMatrix != null && !serviceResolutionMatrix.equals(currentServiceResolutionMatrix)) )
        {
            logger.fine("Adaptation decided!");
System.out.println("Adaptation decided!\n");
            try
            {
                logger.finest("Attempting to apply new configuration: " + serviceResolutionMatrix);

                if(configurationService == null)
                {
                    throw new RuntimeException("Cannot reconfigure system: configuration service is not available!");
                }
                configurationService.applyServiceResolutionMatrix(serviceResolutionMatrix);

                // if we reach this point, then the attempted configuration has succeeded
                currentServiceResolutionMatrix = serviceResolutionMatrix;
            }
            catch (ConfigurationException ce)
            {
                logger.info("Could not reconfigure system! " + ce.getMessage());
            }
        }
        else
        {
            logger.fine("No adaptation resulted!");

System.out.println("No adaptation resulted!\n");
        }
    }

    // ------------------ End of main adaptation steps ---------------------- //
}